Link to this headingReverse Proxy

URL:

Web ServerBehavior
Apache/foo;name=orange/bar/
Nginx/foo;name=orange/bar/
IIS/foo;name=orange/bar/
Tomcat/foo/bar/
Jetty/foo/bar/
WildFly/foo
WebLogic/foo

GET //test/../%2e%2e%2f<>.JpG?a1=”&?z#/admin/

Sources:

Link to this headingNginx

  • case-sensitive for verb (400 error)
  • doesn’t treat // as a directory (/images/1.jpg/..//../1.jpg -> /1.jpg)
  • doesn’t allow in the path: %00 0x00 %
  • doesn’t allow %2f as the first slash
  • doesn’t path normalize /..
  • doesn’t allow underscore (_) in header name (doesn’t forward it)

Link to this headingFingerprint

400 error:

<html> <head><title>400 Bad Request</title></head> <body bgcolor="white"> <center><h1>400 Bad Request</h1></center> <hr><center>nginx/1.15.3</center> </body> </html>

403 error:

<html> <head><title>403 Forbidden</title></head> <body bgcolor="white"> <center><h1>403 Forbidden</h1></center> <hr><center>nginx</center> </body> </html>

Link to this headingAbsolute-URI

  • supports Absolute-URI with higher priority under host header
  • any scheme in Absolute-URI
  • doesn’t allow @ in Absolute-URI (400 error)

Link to this headingLocation match rules

  • (none): If no modifiers are present, the location is interpreted as a prefix match. This means that the location given will be matched against the beginning of the request URI to determine a match.
  • =: If an equal sign is used, this block will be considered a match if the request URI exactly matches the location given.
  • ~: If a tilde modifier is present, this location will be interpreted as a case-sensitive regular expression match.
  • ~*: If a tilde and asterisk modifier is used, the location block will be interpreted as a case-insensitive regular expression match.
  • ^~: If a carat and tilde modifier is present, and if this block is selected as the best non-regular expression match, regular expression matching will not take place.

Understanding nginx server and location block selection algorithms

Link to this headingproxy_pass

  • backend (URL to origin) is uncontrollable
  • parses, url-decodes, normalizes, finds location
    • cut off #fragment
    • doesn’t normalize /..
    • // -> /
  • if trailing slash is in proxy_pass(proxy_pass http://backend/), it forwards the processed request(path)
    • %01-%FF in path -> !"$&'()*+,-./:;<=>@[\]^_`{|}~, 0-9, a-Z, %23 %25 %3F, %01-20, =>%7F
      • %2f to /, which useful for %2f..
      • <> ' " - useful for xss
  • if no trailing slash is in proxy_pass (proxy_pass http://backend), it forwards the initial request(path)
    • /!"$&'()*+,-./:;<=>@[\]^_`{|}~?a#z -> /!"$&'()*+,-./:;<=>@[\]^_`{|}~?a#z
    • %01-%FF -> %01-%FF
  • proxy_pass http://$host/ (with ending /) doesn’t proxy path-part
  • forwards raw bytes (0x01-0x20, > 0x80) in path as-is
  • set HTTP/1.0 by default
  • $host - from the request’s Host header ; $http_host- host from config (default)
  • allows >1 Host header
    • forwards only the first one
  • doesn’t forward headers with space symbols in name ( AnyHeader: or AnyHeader :)
  • no additional headers to backend

Link to this headingrewrite

  • similar to proxy_pass with trailing slash
  • %0a cuts the path
    • /rewrite_slash/123%0a456?a=b -> /rewrite_slash/123?a=b
location /rewrite_slash/ { rewrite /rewrite_slash/(.*) /$1 break; proxy_pass http://backend:9999/; }

Link to this headingCaching

  • Nginx only caches GET and HEAD requests
  • It respects the Cache-Control and Expires headers from origin server
    • It does not cache responses with Cache-Control set to Private, No-Cache, or No-Store or with Set-Cookie in the response header.
  • Does not honor the Pragma and the client’s Cache-Control
  • Doesn’t care about Vary header
  • key for cache: host header and path+query
    • #- is ordinary symbol here

Caching detections:

  • X-Cache-Status: MISS - custom header which shows caching
  • If caching is enabled, the header fields “If-Modified-Since”, “If-Unmodified-Since”, “If-None-Match”, “If-Match”, “Range”, and “If-Range” from the original request are not passed to the origin server.
  • doesn’t care If-Match for uncached content
  • cares If-Match for cached content:
    • W/“0815” - returns 412 Precondition Failed
    • If-Match: * returns body
  • doesn’t care Range headers

Link to this headingVulnerable configs

  • one level traversal
    • /host_noslash_path../something/ -> /lala/../something/
location /host_noslash_path { proxy_pass http://192.168.78.111:9999/lala/; }
  • no first /
    • /without/slash/here -> GET without/slash/here HTTP/1.1
    • (absolute uri?)
rewrite /(.*) $1 break;

Link to this headingApache

  • case-sensitive for verb (get != GET)
    • insensitive with PHP
  • treats // as a directory (/images/1.jpg/..//../2.jpg -> /images/2.jpg)
  • doesn’t allow in path: # % %00
  • doesn’t allow %2f in path (default config: AllowEncodedSlashes Off)
    • %2f is always 404 (/%2f/../index.php/ or /index.php/%2f)
  • can be the forward-proxy
  • support this request (points to root) GET ? HTTP/1.1
  • cares about cache check headers (If-Range/Match/*)
    • doesn’t care in case of PHP
  • If-Range + Range -> returns part of content only if If-Range correct
  • No Accept-Ranges: bytes in case of php
  • doesn’t allow underscore (_) in headers (skips)

Link to this headingFingerprinting

400 error:

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>400 Bad Request</title> </head><body> <h1>Bad Request</h1> <p>Your browser sent a request that this server could not understand.<br /> </p> </body></html>

403 error:

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>403 Forbidden</title> </head><body> <h1>Forbidden</h1> <p>You don't have permission to access / on this server.<br /> </p> </body></html>

Absolute-URI:

  • supports Absolute-URI with higher priority than host header
  • any scheme in Absolute-URI
  • doesn’t like @ in Absolute-URI (400 error)

Link to this headingLocation match rules (the same works for ProxyPass)

http://httpd.apache.org/docs/current/mod/core.html#location

  • Every type is case-sensitive

  • <Directory>
    Is used to enclose a group of directives that will apply only to the named directory, sub-directories of that directory, and the files within the respective directories. Any directive that is allowed in a directory context may be used. Directory-path is either the full path to a directory, or a wild-card string using Unix shell-style matching. In a wild-card string, ? matches any single character, and * matches any sequences of characters. You may also use [] character ranges.

  • <Location "/private1">
    The specified location matches exactly the path component of the URL.
    The specified location, which ends in a forward slash, is a prefix of the path component of the URL (treated as a context root). The specified location, with the addition of a trailing slash, is a prefix of the path component of the URL (also treated as a context root).

    • /private1 -> /private1, /private1/ and /private1/file.txt
    • /private2/ -> /private2/ , /private2/file.txt
  • The <LocationMatch> directive and the regex version of <Location> require you to explicitly specify multiple slashes if that is your intention.

    • <LocationMatch> == <Location ~ "/(extra|special)/data">
    • Location with support of RegExps
    • <LocationMatch "^/abc"> would match the request URL /abc but not the request URL //abc. The (non-regex) <Location> directive behaves similarly when used for proxy requests. But when (non-regex) <Location> is used for non-proxy requests it will implicitly match multiple slashes with a single slash. For example, if you specify <Location "/abc/def"> and the request is to /abc//def then it will match.
  • FilesMatch and Files to set rules for extensions, but works for only inside current location (<FilesMatch \.php$> in virt host -> /test.php - OK, /anything/test.php - no)

Link to this headingProxyPass

  • backend (URL to origin) is controllable
  • doesn’t care about the verb
  • parses, url-decodes, normalizes, finds location, url-encodes
    • /.. - > /../
    • // -> // (except the first / symbol)
      • //path -> /path
      • /path// -> /path//
    • !"$&'()*+,-./:;<=>@[\]^_`{|}~ -> rev proxy -> !%22$&'()*+,-./:;%3C=%3E@%5B%5C%5D%5E_%60%7B%7C%7D~
    • %01-%FF in path -> !$&'()*+,-.:;=@_~, 0-9, a-Z, others are URL-encoded
  • doesn’t allow >1 Host header
  • doesn’t forward with trailing space AnyHeader :
  • support line folding for headers ( Header:zzz-> it is concatenated with the previous header)
  • doesn’t forward Host, sets value from ProxyPass
  • adds headers to request to origin: X-Forwarded-For: , X-Forwarded-Host: , X-Forwarded-Server:
    • we can send our values in request and it will be added to proxy’s request (examplezzz.com, example2.com)
  • adds Content-Type depending on extension, if there is no CT from origin server

Link to this headingRewrite

  • flags https://httpd.apache.org/docs/2.4/rewrite/flags.html
  • similar to ProxyPas, but:
  • url decodes values, flag B encodes them again
  • url decodes, normalizes, then put in url and parse it
    • %0a cuts the path
      • /lala/123%0a456?a=b -> /lala/123?a=b
    • %01-%FF in path -> !$&'()*+,-.:;=?@[\]^_`{|}~, a-Z, 0-9, >0x7F, others are URL encoded
    • %3f decoded to ?, but %3faa=1?bb=2 -> ?aa=1
    • inside (.*), /lala/path/%2e%2e -> /path/.. (it’s not normalized, but /path/%2e%2e/ - is)
    • !"$&'()*+,-./:;<=>@[\]^_`{|}~ -> rev proxy -> !%22$&'()*+,-./:;%3C=%3E@%5B%5C%5D%5E_%60%7B%7C%7D~
<VirtualHost *:80> ServerName example1.com RewriteEngine On RewriteRule /lala/(.*) http://192.168.78.111:9999/$1 [P,L] </VirtualHost>

Link to this headingVulnerable configs

RewriteEngine On RewriteCond %{REQUEST_URI} ^/neighborhood/[^/]+/feed$ [NC] RewriteRule ^.*$ - [F,L]
  • No ending slash SSRF (incorrect config)
    • /@evil.com/index.php
    • /.evil.com/index.php
<VirtualHost *:80> ServerName example0.com ProxyPass / http://192.168.78.111 </VirtualHost>

Link to this headingCaching

not tested

Link to this headingHProxy and Nuster

Link to this headingBasics

  • case-insensitive for verb
  • allows any path/query values (except 0x00-0x20, >0x80):
    • GET !i?lala=#anything HTTP/1.1
  • doesn’t url-decode and normalize the path before applying rules
  • support converters:
    • url_dec - url decodes (but sends undecoded to origin server), but spoils path_begin
  • path_* extracts the path, which starts at the first slash and ends before the first question mark
  • allows >1 Host:
    • forwards all of them
  • doesn’t forward AnyHeader : - 400 error
  • support line folding for headers ( Header:zzz-> concatenate with previous header)
  • no additional headers to backend

Link to this headingFingerprint

  • no special headers

400 error:

<html><body><h1>400 Bad request</h1> Your browser sent an invalid request. </body></html>

403 error:

<html><body><h1>403 Forbidden</h1> Request forbidden by administrative rules. </body></html>

Link to this headingAbsolute-URI

Link to this headingCaching

Cache’s been partly implemented in this version of HAproxy. It was not tested. Nuster was tested instead

  • default key of CACHE: method.scheme.host.uri
  • default key of NoSQL: GET.scheme.host.uri
  • only 200 response is cached
  • doesn’t respect Cache-Control, Expire headers from the origin
  • Does not honor the Pragma and the client’s Cache-Control

Link to this headingVulnerable configs

  • Bypass //admin/ /Admin/ /%61dmin/
acl restricted_page path_beg /admin
  • Bypass /log/ - any trailing symbol (e.g. /)
acl restricted_page path_beg,url_dec /log

Link to this headingVarnish

Link to this headingBasics

  • backend (URL to origin) is uncontrollable
  • allows any value for the verb
  • allows any path/query values (except 0x00-0x20): GET !i?lala=#anything HTTP/1.1
  • doesn’t normalize, url-decode request before applying rules
  • doesn’t forward AnyHeader : - 400
  • support line folding for headers ( Header:zzz-> concatenate with previous header)
  • doesn’t allow >1 Host:
  • forwards Host: header
  • adds X-Forwarded-For: to req to the origin server
    • we can send our values in request and it will be added to proxy’s request (examplezzz.com, example2.com)
  • req includes query string (no path part)
    • req.url ~ "\.jpg$" == ?random=.jpg

Link to this headingFingerprint

  • Via: 1.1 varnish (Varnish/5.0)
  • X-Varnish: 7
  • X-Varnish-Host: ip-address-here
  • X-Varnish-Backend: ip address
  • X-Varnish-Esi-Method
  • Accept-Range: bytes (for all requests)
  • 400 error: HTTP/1.1 400 Bad Request

Link to this headingAbsolute-URI

  • support Absolute-URI with higher priority under host header
  • “http” only in Absolute-URI

Link to this headingCaching

  • it caches GET and HEAD requests
  • key for cache: host header and uri
  • doesn’t cache reqs with cookie(!) or Authorization header, or Set-Cookie (default)
    • often practice to cut all cookie headers before sending to origin
  • It respects the Cache-Control and Expires headers from origin server (depending on version)
    • it respects CC flags
    • it cares about the max-age parameter and uses it to calculate the TTL for an object.
    • it ignores “Cache-Control: no-cache” by default, but cares about “max-age” (Before V4.0.0?)
  • Does not honor the Pragma and the client’s Cache-Control
  • doesn’t care about Vary, by default

Link to this headingCaching detection

  • X-Varnish: has two figures in case of hit, and one in case of miss. Age is also changed (0 -> \d+ )
X-Varnish: 65563 29 X-Varnish: 65563
  • Age: 0
  • doesn’t care about If-* headers
  • support Range header
  • If-Range + Range -> returns part of content only (always)

Link to this headingVulnerable configs

  • Misrouting /../admin/
if (req.http.host == "sport.example.com") { set req.http.host = "example.com"; set req.url = "/sport" + req.url; }
  • Blacklist bypass Post //wp-login.php HTTP/1.1
if(req.method == "POST" || req.url ~ "^/wp-login.php$" || req.url ~ "^/wp-admin") { return(synth(503)); }